Статьи

Еще 10 распространенных ошибок, которые делают Java-разработчики при написании SQL

Я был приятно удивлен, увидев, насколько популярным был мой недавний список о  10 распространенных ошибках, которые делают Java-разработчики при написании SQL  , как  в моем собственном блоге, так  и  в моем партнере по синдикации DZone . Популярность показывает пару вещей:

  • Насколько важен SQL для профессионального мира Java.
  • Как часто забывают о некоторых элементах SQL.
  • Насколько хорошо SQL-ориентированные библиотеки, такие как  jOOQ  или  MyBatis  , реагируют на потребности рынка, используя  SQL . Забавный факт заключается в том, что пользователи даже упомянули мой пост  в списке рассылки SLICK . SLICK — не-SQL-ориентированная библиотека доступа к базам данных в Scala. Как и LINQ (и LINQ-to-SQL), он сосредоточен на языковой интеграции , а не на генерации кода SQL.

В любом случае, распространенные ошибки, которые я перечислил ранее, далеки от завершения, поэтому я расскажу вам о 10 менее заметных, но не менее интересных ошибках, которые допускают Java-разработчики при написании SQL.

1. Не использовать PreparedStatements

Интересно, что эта ошибка или заблуждение все еще всплывают в  блогахфорумах  и списках рассылки через много лет после появления JDBC, даже если речь идет об очень простой вещи, которую нужно запомнить и понять. Похоже, что некоторые разработчики воздерживаются от использования PreparedStatements по любой из следующих причин:

  • Они не знают о PreparedStatements
  • Они думают, что PreparedStatements медленнее
  • Они думают, что написание PreparedStatement требует больше усилий

Во-первых, давайте разберем вышеупомянутые мифы. В 96% случаев лучше писать PreparedStatement, а не статический оператор. Зачем? По простым причинам:

  • Вы можете пропустить синтаксические ошибки, возникающие из-за неправильной конкатенации строк при встраивании значений связывания.
  • Вы можете исключить уязвимости SQL-инъекций из-за плохой конкатенации строк при встраивании значений связывания.
  • Вы можете избежать крайних случаев, когда встраиваете более «сложные» типы данных, такие как TIMESTAMP, двоичные данные и другие.
  • Вы можете некоторое время держать открытыми PreparedStatements, повторно используя их с новыми значениями связывания, вместо того, чтобы закрывать их немедленно (полезно, например, в Postgres).
  • Вы можете использовать адаптивное совместное использование  курсоров  (на языке Oracle) в более сложных базах данных. Это помогает предотвратить жесткий синтаксический анализ операторов SQL для каждого нового набора значений связывания.

Будучи убеждена? Да. Обратите внимание: в некоторых редких случаях вам действительно необходимо встроить значения привязки, чтобы оптимизатор на основе затрат вашей базы данных получил некоторую информацию о том, на какие данные действительно повлияет запрос. Как правило, это приводит к «постоянным» предикатам, таким как:

  • УДАЛЕНО = 1
  • СОСТОЯНИЕ = 42

Но это не должно приводить к «переменным» предикатам, таким как:

  • FIRST_NAME НРАВИТСЯ «Jon%»
  • СУММА> 19,95

Обратите внимание, что современные базы данных реализуют просмотр переменных bind Следовательно, по умолчанию вы также можете использовать значения связывания для всех параметров запроса. Также обратите внимание, что высокоуровневые API, такие как  JPA CriteriaQuery  или  jOOQ  , помогут вам генерировать PreparedStatements и очень просто и прозрачно связывать значения при написании встроенного JPQL или встроенного SQL.

Дополнительная справочная информация:

The Cure :

По умолчанию всегда используйте PreparedStatements вместо статических операторов. По умолчанию никогда не вставляйте встроенные значения в ваш SQL.

2. Возврат слишком большого количества столбцов

Эта ошибка встречается довольно часто и может привести к очень плохим последствиям как в плане выполнения вашей базы данных, так и в вашем приложении Java. Давайте сначала посмотрим на второй эффект:

Плохое влияние на Java-приложение:

Если вы выбираете * (звездочка) или набор из 50 столбцов «по умолчанию», который вы повторно используете в различных DAO, вы переносите много данных из базы данных в JDBC ResultSet. Даже если вы не читаете данные из ResultSet, они были переданы по проводам и загружены в вашу память драйвером JDBC. Это пустая трата ввода-вывода и памяти, если вы  знаете,  что вам понадобятся только 2-3 из этих столбцов.

Это было очевидно, но остерегайтесь также …

Плохое влияние на план выполнения базы данных:

Эти эффекты могут быть намного хуже, чем эффекты на Java-приложении. Сложные базы данных выполняют много преобразований SQL при расчете наилучшего плана выполнения для вашего запроса. Вполне возможно, что некоторые части вашего запроса можно «преобразовать», зная, что они не будут вносить вклад в проекцию (предложение SELECT) или в предикаты фильтрации. Недавно я писал об этом в контексте метаданных схемы:
как метаданные схемы влияют на преобразования запросов Oracle

Теперь это настоящий зверь. Подумайте о сложном SELECT, который объединит два представления:

SELECT *
FROM   customer_view c
JOIN   order_view o
  ON   c.cust_id = o.cust_id

Каждое из представлений, которые присоединяются к указанной выше ссылке на объединенную таблицу, может снова объединять данные из десятков таблиц, таких как CUSTOMER_ADDRESS, ORDER_HISTORY, ORDER_SETTLEMENT и т. Д. Учитывая проекцию SELECT *, ваша база данных не имеет другого выбора, кроме как полностью выполнить загрузку все эти объединенные таблицы, когда единственное, что вас интересовало, это:

SELECT c.first_name, c.last_name, o.amount
FROM   customer_view c
JOIN   order_view o
  ON   c.cust_id = o.cust_id

Хорошая база данных преобразует ваш SQL таким образом, что большинство «скрытых» объединений может быть удалено, что приводит к гораздо меньшему количеству операций ввода-вывода и потребления памяти в базе данных.

The Cure :

Никогда не выполняйте SELECT *. Никогда не используйте один и тот же прогноз для различных запросов. Всегда старайтесь уменьшить проекцию на данные, которые вам действительно нужны.

Обратите внимание, что это может быть довольно трудно достичь с помощью ORM.

3. Думая, что JOIN является предложением SELECT

Это не ошибка, которая сильно влияет на производительность или корректность SQL, но, тем не менее, разработчики SQL должны учитывать тот факт, что предложение JOIN не является частью оператора SELECT как такового. Стандарт  SQL 1992  определяет  table reference как таковой:

6.3 <table reference>

<table reference> ::=
    <table name> [ [ AS ] <correlation name>
      [ <left paren> <derived column list> <right paren> ] ]
  | <derived table> [ AS ] <correlation name>
      [ <left paren> <derived column list> <right paren> ]
  | <joined table>

В  FROM предложении, а также в  joined table банке можно использовать такие  table references:

7.4 <from clause>

<from clause> ::= 
    FROM <table reference> [ { <comma> <table reference> }... ]

7.5 <joined table>

<joined table> ::=
    <cross join>
  | <qualified join>
  | <left paren> <joined table> <right paren>

<cross join> ::=
    <table reference> CROSS JOIN <table reference>

<qualified join> ::=
    <table reference> [ NATURAL ] [ <join type> ] JOIN
      <table reference> [ <join specification> ]

Реляционные базы данных очень ориентированы на таблицы. Многие операции выполняются с физическими, объединенными или производными таблицами тем или иным способом. Чтобы эффективно писать SQL, важно понимать, что в этом  SELECT .. FROM предложении ожидается разделенный запятыми список ссылок на таблицы в любой форме, в которой они могут быть предоставлены.

В зависимости от сложности ссылки на таблицу, некоторые базы данных также принимают сложные ссылки на таблицы в других операторах, таких как INSERT, UPDATE, DELETE, MERGE. См.  , Например , руководства Oracle , объясняющие, как создавать обновляемые представления.

The Cure :

Всегда думайте о своем  FROM предложении, чтобы ожидать ссылки на таблицу в целом. Если вы пишете  JOIN предложение, думайте, что это  JOIN предложение является частью сложной ссылки на таблицу:

SELECT c.first_name, c.last_name, o.amount
FROM
 
    customer_view c
      JOIN order_view o
      ON c.cust_id = o.cust_id

4. Использование pre-ANSI JOIN синтаксиса

Теперь, когда мы выяснили, как работают ссылки на таблицы (см. Предыдущий пункт), должно стать немного более очевидным, что синтаксис pre-ANSI JOIN следует избегать любой ценой. Для планов выполнения обычно не имеет значения, если вы указываете предикаты соединения в  JOIN .. ON предложении или в WHERE предложении. Но с точки зрения удобочитаемости и обслуживания использование  WHERE условия как для предикатов фильтрации, так и для предикатов соединения является серьезной проблемой. Рассмотрим этот простой пример:

SELECT c.first_name, c.last_name, o.amount
FROM   customer_view c,
       order_view o
WHERE  o.amount > 100
AND    c.cust_id = o.cust_id
AND    c.language = 'en'

Можете ли вы определить предикат соединения? Что, если мы присоединились к десяткам столов? Это становится намного хуже при применении проприетарных синтаксисов для внешнего объединения, таких как синтаксис Oracle (+) .

The Cure :

Всегда используйте синтаксис ANSI JOIN. Никогда не помещайте предикаты JOIN в  WHERE предложение. Использование синтаксиса pre-ANSI JOIN абсолютно бесполезно.

5. Забыть экранирование от предиката LIKE

Стандарт  SQL 1992  определяет  like predicate как таковой:

8.5 <like predicate>

<like predicate> ::=
    <match value> [ NOT ] LIKE <pattern>
      [ ESCAPE <escape character> ]

ESCAPE Ключевое слово следует использовать почти всегда , когда позволяет пользовательский ввод , которые будут использоваться в запросах SQL. Хотя это может быть редко, когда знак процента (%) фактически должен быть частью данных, подчеркивание (_) вполне может быть:

SELECT *
FROM   t
WHERE  t.x LIKE 'some!_prefix%' ESCAPE '!'

The Cure :

Всегда думайте о правильном экранировании при использовании предиката LIKE.

6. Думая, что NOT (A IN (X, Y)) является булевой инверсией A IN (X, Y)

Это тонкий, но очень важный по отношению к NULL! Давайте рассмотрим, что на  A IN (X, Y) самом деле означает:

                  A IN (X, Y)
is the same as    A = ANY (X, Y)
is the same as    A = X OR A = Y

Когда в то же время  NOT (A IN (X, Y)) действительно означает:

                  NOT (A IN (X, Y))
is the same as    A NOT IN (X, Y)
is the same as    A != ANY (X, Y)
is the same as    A != X AND A != Y

Это выглядит как логическое обратное к предыдущему предикату, но это не так! Если любой из  X или  Y есть  NULLNOT IN предикат будет приводить к,  UNKNOWN тогда как IN предикат может все еще возвращать логическое значение.

Или, другими словами, когда  A IN (X, Y) дает  TRUE или  FALSENOT(A IN (X, Y)) может все еще давать  UNKNOWN вместо  FALSE или  TRUE. Обратите внимание, что это также верно, если правая часть  IN предиката является подзапросом.

Не верь этому? Посмотрите эту  SQL Fiddle  для себя. Это показывает, что следующий запрос не дает результата:

SELECT 1
WHERE     1 IN (NULL)
UNION ALL
SELECT 2
WHERE NOT(1 IN (NULL))

Более подробную информацию можно увидеть в  моем предыдущем посте на эту тему , где также показаны некоторые несовместимости диалектов SQL в этой области.

The Cure :

Остерегайтесь  NOT IN предикатов, когда участвуют обнуляемые столбцы!

7. Думая, что NOT (A IS NULL) совпадает с A NOT NULL

Итак, мы вспомнили, что SQL реализует трехзначную логику, когда дело доходит до обработки NULL. Вот почему мы можем использовать предикат NULL для проверки значений NULL. Правильно? Правильно.

Но даже предикат NULL является тонким. Помните, что два следующих предиката эквивалентны только для выражений значения строки степени 1:

                   NOT (A IS NULL)
is not the same as A IS NOT NULL

Если A является выражением значения строки со степенью больше 1, то таблица истинности преобразуется так, что:

  • A IS NULL возвращает true, только если все значения в A равны NULL
  • NOT (A IS NULL) возвращает false, только если все значения в A равны NULL
  • A NOT NOT NULL возвращает true, только если все значения в A не равны NULL
  • NOT (A NOT NOT NULL) возвращает false, только если все значения в A не равны NULL

Подробности смотрите в моем  предыдущем посте на эту тему .

The Cure :

При использовании выражений значения строки остерегайтесь предиката NULL, который может работать не так, как ожидалось.

8. Не использовать выражения значения строки там, где они поддерживаются

Выражения значений строк — это отличная функция SQL. Когда SQL является языком, очень ориентированным на таблицы, таблицы также очень ориентированы на строки. Выражения значений строк позволяют гораздо проще описывать сложные предикаты, создавая локальные специальные строки, которые можно сравнивать с другими строками той же степени и типа строки. Простой пример — запросить у клиентов имена и фамилии одновременно.

SELECT c.address
FROM   customer c,
WHERE (c.first_name, c.last_name) = (?, ?)

Как можно видеть, этот синтаксис немного более лаконичен, чем эквивалентный синтаксис, где каждый столбец с левой стороны предиката сравнивается с соответствующим столбцом с правой стороны. Это особенно верно, если многие независимые предикаты объединяются с AND. Использование выражений значений строк позволяет объединять коррелированные предикаты в один. Это наиболее полезно для выражений соединения на составных внешних ключах:

SELECT c.first_name, c.last_name, a.street
FROM   customer c
JOIN   address a
  ON  (c.id, c.tenant_id) = (a.id, a.tenant_id)

К сожалению, не все базы данных поддерживают выражения значений строк одинаково. Но стандарт SQL определил их уже в  1992 году , и если вы используете их, сложные базы данных, такие как Oracle или Postgres, могут использовать их для расчета более эффективных планов выполнения. Это объясняется на популярной   странице « Использование индекса», Люк .

The Cure :

Используйте выражения значения строки всякий раз, когда можете. Они сделают ваш SQL более кратким и, возможно, даже быстрее.

9. Не достаточно определения ограничений

Итак, я собираюсь процитировать  Тома Кайта  и   снова использовать «Индекс», Люк . Вы не можете иметь достаточно ограничений в своих метаданных. Прежде всего, ограничения помогают вам предотвратить повреждение данных, что уже очень полезно. Но для меня, что более важно, ограничения помогут базе данных выполнять преобразования SQL, так как база данных может решить, что

  • Некоторые значения эквивалентны
  • Некоторые пункты излишни
  • Некоторые пункты являются «пустыми» (т.е. они не будут возвращать никаких значений)

Некоторые разработчики могут подумать, что ограничения медленные. Наоборот, если вы не вставляете много-много данных, в случае чего вы можете либо отключить ограничения для большой операции, либо использовать временную «таблицу загрузки» без ограничений, перенося данные в автономный режим в реальную таблицу.

The Cure :

Определите столько ограничений, сколько сможете. Они помогут вашей базе данных работать лучше при запросах.

10. Думая, что 50 мс — быстрое выполнение запроса

Шумиха вокруг NoSQL все еще продолжается, и многие компании по-прежнему считают, что им Twitter или Facebook крайне необходимы более быстрые и масштабируемые решения, позволяющие избежать масштабирования по горизонтали с помощью ACID и реляционных моделей. Некоторые могут преуспеть (например, Twitter или Facebook), другие могут столкнуться с этим:

Для тех, кто вынужден (или выбрал) придерживаться проверенных реляционных баз данных, не думайте, что современные базы данных работают медленно. Они очень быстрые. На самом деле они настолько быстрые, что могут анализировать ваш 20-килобайтный текст запроса, рассчитывать планы выполнения на 2000 строк и фактически выполнять этого монстра менее чем за миллисекунду, если вы и ваш администратор баз данных хорошо ладите и настраиваете свою базу данных на максимум ,

Они могут быть медленными из-за того, что ваше приложение неправильно использует  популярный ORM или потому что ORM не сможет генерировать быстрый SQL для вашей сложной логики запросов. В этом случае вы можете выбрать более ориентированный на SQL API, такой как  JDBCjOOQ  или  MyBatis  , который позволит вам вернуть контроль над SQL.

Поэтому не думайте, что выполнение запроса в 50 мс является быстрым или даже приемлемым. Это не. Если вы получаете эти скорости во время разработки, обязательно изучите планы выполнения. Эти монстры могут взорваться в производстве, где у вас есть более сложные контексты и данные.

Вывод

SQL — это очень весело, но очень тонко и по-разному. Нелегко сделать это правильно, как  показал мой предыдущий пост в блоге о  10 распространенных ошибках . Но SQL можно освоить, и оно того стоит. Данные — ваш самый ценный актив. Относитесь к данным с уважением и лучше пишите SQL.