Почему «Выбрать * из таблицы» считается плохой практикой


96

Вчера я обсуждал с программистом «хобби» (я сам профессиональный программист). Мы сталкивались с некоторыми из его работ, и он сказал, что он всегда запрашивает все столбцы в своей базе данных (даже на / в производственном сервере / коде).

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


1
Я бы сказал, потому что, если содержимое таблицы изменится? добавление / удаление столбцов? вы по-прежнему выбираете * .. так что вы будете пропускать вещи или извлекать больше данных, чем вам нужно.
JF это

2
@JFit Это часть этого, но далеко не вся история.
jwenting



@gnat можно ли считать вопрос действительно дубликатом закрытого вопроса? (т. е. потому что закрытый не был действительно подходящим с самого начала)
gbjbaanb

Ответы:


67

Подумайте о том, что вы получаете, и как вы связываете это с переменными в вашем коде.

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

Использование select *, когда вы печатаете запросы вручную, не подходит, когда вы пишете запросы для кода.


8
Производительность, нагрузка на сеть и т. Д. Гораздо важнее, чем удобство возврата столбцов в нужном порядке и с нужным именем.
jwenting

21
@ на самом деле? производительность важнее правильности? Во всяком случае, я не вижу, что «select *» работает лучше, чем выбор только тех столбцов, которые вы хотите.
gbjbaanb

9
@Bratch, в реальных производственных средах у вас могут быть сотни приложений, использующих одни и те же таблицы, и невозможно обеспечить надлежащее обслуживание всех этих приложений. Вы правы в своих чувствах, но практически, аргумент терпит неудачу только из-за реалий работы в компаниях. Изменения схемы в активных таблицах происходят постоянно.
user1068

18
Я не понимаю смысла в этом ответе. Если вы добавляете столбец в таблицу, то и SELECT *, и SELECT [Columns] будут работать, единственное отличие состоит в том, что если код должен привязываться к новому столбцу, SELECT [Columns] необходимо будет изменить, тогда как SELECT * не будет. Если столбец удаляется из таблицы, SELECT * будет разрываться в точке привязки, тогда как SELECT [Columns] будет прерываться при выполнении запроса. Мне кажется, что SELECT * является более гибким вариантом, так как любые изменения в таблице потребуют только изменений в привязке. Я что-то пропустил?
TallGuy

11
@gbjbaanb затем получите доступ к столбцам по имени. Все остальное будет явно глупо, если вы не укажете порядок столбцов в запросе.
user253751

179

Изменения схемы

  • Выборка по порядку --- Если код выбирает номер столбца как способ получения данных, изменение в схеме приведет к перенастройке номеров столбцов. Это испортит приложение, и произойдут плохие вещи.
  • Выборка по имени --- Если код выбирает столбец по имени, например foo, а другая таблица в запросе добавляет столбец foo, то, как это обрабатывается, может вызвать проблемы при попытке получить правильный foo столбец.

В любом случае, изменение схемы может вызвать проблемы с извлечением данных.

Далее рассмотрим, удаляется ли использованный столбец из таблицы. Все select * from ...еще работает, но выдает ошибки при попытке извлечь данные из набора результатов. Если в запросе указан столбец, запрос выдаст ошибку, вместо этого дается четкое указание на то, в чем и где проблема.

Затраты на данные

С некоторыми столбцами может быть связано значительное количество данных. Выбор назад *потянет все данные. Да, вот что varchar(4096)на 1000 строк, которые вы выбрали, дает вам дополнительные 4 мегабайта данных, которые вам не нужны, но все равно отправляются по проводам.

В связи с изменением схемы, varchar может не существовать там, когда вы впервые создали таблицу, но теперь она там.

Неспособность передать намерение

Когда вы выбираете обратно *и получаете 20 столбцов, но вам нужно только 2 из них, вы не передаете смысл кода. Глядя на запрос, который делает, select *никто не знает, каковы его важные части. Могу ли я изменить запрос на использование этого другого плана, чтобы ускорить его, не включая эти столбцы? Я не знаю, потому что цель того, что возвращает запрос, не ясна.


Давайте посмотрим на некоторые скрипты SQL, в которых рассматриваются эти изменения схемы .

Во-первых, исходная база данных: http://sqlfiddle.com/#!2/a67dd/1

DDL:

create table one (oneid int, data int, twoid int);
create table two (twoid int, other int);

insert into one values (1, 42, 2);
insert into two values (2, 43);

SQL:

select * from one join two on (one.twoid = two.twoid);

И столбцы вы получаете обратно являются oneid=1, data=42, twoid=2, и other=43.

Что произойдет, если я добавлю столбец к первой таблице? http://sqlfiddle.com/#!2/cd0b0/1

alter table one add column other text;

update one set other = 'foo';

И мои результаты от того же самого запроса , как и раньше являются oneid=1, data=42, twoid=2, и other=foo.

Изменение в одной из таблиц нарушает значения a, select *и внезапно ваша привязка 'other' к int приведет к ошибке, и вы не знаете почему.

Если вместо этого ваш оператор SQL был

select 
    one.oneid, one.data, two.twoid, two.other
from one join two on (one.twoid = two.twoid);

Изменение в таблице один не нарушило бы ваши данные. Этот запрос выполняется одинаково до изменения и после изменения.


индексирование

Когда вы делаете a, select * fromвы вытягиваете все строки из всех таблиц, которые соответствуют условиям. Даже таблицы, которые вам действительно безразличны. Хотя это означает, что передается больше данных, существует еще одна проблема с производительностью, которая скрывается в стеке.

Индексы. (связано с SO: Как использовать индекс в операторе выбора? )

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

Если вы просто выбираете, скажем, фамилию пользователя (которую вы много делаете, и поэтому у вас есть индекс), база данных может выполнять сканирование только по индексу ( postgres wiki index only scan , mysql full table scan vs full сканирование индекса , индекс-Only Scan: Избежание таблицы Access ).

Существует довольно много оптимизаций относительно чтения только из индексов, если это возможно. Информация может быть получена быстрее на каждой странице индекса, потому что вы также извлекаете ее меньше - вы не используете все остальные столбцы для select *. При сканировании только по индексу возможно возвращать результаты в 100 раз быстрее (источник: Select * is bad ).

Это не говорит о том, что полное индексное сканирование - это хорошо, это все же полное сканирование, но это лучше, чем полное сканирование таблицы. Как только вы начинаете преследовать все способы, которые select *ухудшают производительность, вы продолжаете находить новые.

Связанное чтение


2
@ Тонни, я бы согласился - но когда я ответил (сначала), я никогда не думал, что этот вопрос вызовет так много дискуссий и комментариев! Очевидно, что запрашивать только именованные столбцы ?!
gbjbaanb

3
Разбивка всего на части путем добавления столбца также является хорошей причиной, по которой код всегда должен обращаться к столбцам в заголовке данных по имени, а не по жестко заданному порядковому номеру ...
Джулия Хейворд,

1
@gbjbaanb Это для меня. Но многие люди приходят писать SQL-запросы без формальной подготовки / обучения. Для них это не может быть очевидным.
Тонни

1
@Aaronaught Я обновил его с дополнительным битом по вопросам индексации. Есть ли какие-то другие моменты, которые я должен затронуть за неправильность select *?

3
Ничего себе, принятый ответ был настолько плох, чтобы объяснить что-либо, что я за него проголосовал. Поражаюсь, что это не принятый ответ. +1.
Бен Ли

38

Еще одна проблема: если это JOINзапрос, и вы извлекаете результаты запроса в ассоциативный массив (как это может быть в PHP), он подвержен ошибкам.

Дело в том, что

  1. если таблица fooимеет столбцы idиname
  2. если таблица barимеет столбцы idи address,
  3. и в вашем коде вы используете SELECT * FROM foo JOIN bar ON foo.id = bar.id

угадайте, что происходит, когда кто-то добавляет столбец nameв barтаблицу.

Код внезапно перестанет работать должным образом, потому что теперь nameстолбец появляется в результатах дважды, и если вы сохраняете результаты в массиве, данные из second name( bar.name) будут перезаписывать first name( foo.name)!

Это довольно неприятная ошибка, потому что это очень неочевидно. Чтобы понять это, может потребоваться некоторое время, и человек, добавляющий в таблицу еще один столбец, никак не мог предвидеть такой нежелательный побочный эффект.

(Правдивая история).

Поэтому не используйте *, управляйте тем, какие столбцы вы извлекаете, и используйте псевдонимы, где это необходимо.


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

4
Теоретически, но если для удобства вы используете подстановочный знак, вы полагаетесь на него, чтобы автоматически выдавать вам все существующие столбцы, и никогда не удосужились обновить запрос по мере роста таблиц. Если вы указываете каждый столбец, вы вынуждены перейти к запросу, чтобы добавить еще один к вашему SELECTпредложению, и это, когда вы надеетесь найти имя не уникальным. Кстати, я не думаю, что это так редко в системах с большими базами данных. Как я уже сказал, однажды я потратил пару часов на охоту за этой ошибкой в ​​большом коде PHP-кода. И я нашел другой случай только сейчас: stackoverflow.com/q/17715049/168719
Конрад Моравский

3
Я провел час на прошлой неделе, пытаясь понять это через голову консультанта. Он должен быть гуру SQL ... Вздох ...
Тонни

22

Во многих случаях запросы к каждому столбцу могут быть вполне законными.

Всегда запрашивать не каждый столбец.

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

Это больше работы для вашей сети, потому что вы вытягиваете любое количество полей, когда вам может понадобиться только одно или два из них. Если кто-то [еще] идет и добавляет пару дюжин дополнительных полей, каждое из которых содержит большие куски текста, ваша пропускная способность внезапно падает через пол - без видимой причины. Это усугубляется, если ваше предложение «где» не особенно хорошо, и вы также вытягиваете много строк - это потенциально много данных, разбирающихся с вами по сети (т.е. это будет медленно).

Это больше работы для вашего приложения, необходимость откатить и сохранить все эти дополнительные данные, которые, вероятно, не заботятся.

Вы рискуете изменить порядок столбцов. Хорошо, вам не нужно беспокоиться об этом (и вы не будете беспокоиться об этом, если выберете только нужные вам столбцы), но, если вы идете, получите их все сразу, и кто-то [еще] решит изменить порядок столбцов в таблице. Этот тщательно продуманный CSV-экспорт, который вы даете на счета по коридору, внезапно превращается в неудачу - опять же, без видимой причины.

Кстати, я сказал «кто-то [еще]» пару раз выше. Помните, что базы данных по своей сути многопользовательские; Вы можете не иметь контроля над ними, как вы думаете.


3
Я бы подумал, что запросы к каждому столбцу всегда могут быть оправданы для таких вещей, как независимые от схемы средства просмотра таблиц. Не очень распространенная ситуация, но в контексте инструментов, предназначенных только для внутреннего использования, такие вещи могут быть полезны.
суперкат

1
@supercat Это только ЕДИНСТВЕННЫЙ допустимый вариант использования "SELECT *", который я могу придумать. И даже тогда я предпочел бы ограничить запрос «SELECT TOP 10 *» (в MS SQL) или добавить «LIMIT 10» (mySQL) или добавить «WHERE ROWNUM <= 10» (Oracle). Обычно в этом случае речь идет скорее о том, «какие есть столбцы и некоторые примеры данных», чем о полном содержимом.
Тонни

@Tonny: SQL Server изменил свои сценарии по умолчанию, чтобы добавить TOPограничение; Я не уверен, насколько это важно, если код читает столько раз, сколько нужно для отображения, а затем удаляет запрос. Я думаю, что ответы на запросы обрабатываются несколько лениво, хотя я не знаю деталей. В любом случае, я думаю, что вместо того, чтобы говорить, что это "не законно", было бы лучше сказать "... законно гораздо меньше"; По сути, я бы суммировал законные случаи как случаи, когда у пользователя было бы лучшее представление о том, что является значимым, чем программист.
суперкат

@supercat Я могу согласиться с этим. И мне очень нравится, как вы выразили это в своем последнем предложении. Я должен помнить это.
Тонни

11

Краткий ответ: это зависит от того, какую базу данных они используют. Реляционные базы данных оптимизированы для извлечения данных вам нужны в быстром, надежном и атомном пути. В больших наборах данных и сложных запросах это намного быстрее и, вероятно, безопаснее, чем SELECTing *, и делает эквивалент соединений на стороне «кода». Хранилища ключей-значений могут не иметь таких реализованных функций или быть недостаточно зрелыми для использования в производстве.

Тем не менее, вы по-прежнему можете заполнять любую структуру данных, которую вы используете, с помощью SELECT * и обрабатывать все остальное в коде, но вы найдете узкие места в производительности, если захотите масштабировать.

Самое близкое сравнение - сортировка данных: вы можете использовать быструю сортировку или пузырьковую сортировку, и результат будет правильным. Но не будет оптимизирован, и определенно будут проблемы, когда вы вводите параллелизм и должны сортировать атомарно.

Конечно, дешевле добавлять ОЗУ и ЦП, чем вкладывать средства в программиста, который может выполнять запросы SQL и даже имеет смутное представление о том, что такое JOIN.


Изучай SQL! Это не так сложно. Это «родной» язык баз данных повсеместно. Это мощно. Это элегантно Он выдержал испытание временем. И вы никак не сможете написать объединение на стороне «кода», которое будет более эффективным, чем объединение в базе данных, если только вы действительно не умеете делать объединения SQL. Учтите, что для «объединения кода» необходимо извлечь все данные из обеих таблиц даже в простом соединении с двумя таблицами. Или вы извлекаете статистику индекса и используете ее, чтобы решить, какие данные таблицы нужно извлечь перед присоединением? Не думаю, что так ... Научитесь правильно пользоваться базой данных, люди.
Крейг,

@Craig: SQL широко распространен в реляционных базах данных. Это далеко не единственный тип БД, хотя ... и есть причина, почему более современные подходы к базам данных часто называют NoSQL. : P Никто из тех, кого я знаю, не станет называть SQL "элегантным" без большой дозы иронии. Это просто отстой меньше, чем многие из альтернатив, что касается реляционных баз данных.
cHao

@CHao Я очень хорошо знал о различных других типах баз данных на протяжении десятилетий . База данных Pick "nosql" существует всегда. «NoSQL» даже отдаленно не является новой концепцией. ORM также были всегда, и они всегда были медленными. Медленно! = Хорошо. Что касается элегантности (LINQ?), Вы не можете убедить меня, что это разумно или элегантно для предложения «где»: Customer customer = this._db.Customers.Where( “it.ID = @ID”, new ObjectParameter( “ID”, id ) ).First();см. « Время обижаться» на стр. 2.
Крейг,

@Craig: даже не заводите меня на ORM. Почти каждая система там делает это ужасно, и абстракция протекает повсюду. Это связано с тем, что записи в реляционных БД не являются объектами - в лучшем случае они являются сериализуемыми внутренностями части объекта. Но что касается LINQ, вы действительно хотите пойти туда? SQLish эквивалент это что-то вроде var cmd = db.CreateCommand(); cmd.CommandText = "SELECT TOP 1 * FROM Customers WHERE ID = @ID"; cmd.Parameters.AddWithValue("@ID", id); var result = cmd.ExecuteReader();...., а затем приступить к созданию Customer из каждой строки. LINQ бьет штаны от этого.
cHao

@Craig: Конечно, это не так элегантно, как могло бы быть. Но он никогда не будет таким элегантным, как мне бы хотелось, пока он не сможет конвертировать .net код в SQL. :) В какой момент вы могли бы сказать var customer = _db.Customers.Where(it => it.id == id).First();.
cHao

8

ИМО, о том, чтобы быть явным против неявного. Когда я пишу код, я хочу, чтобы он работал, потому что я заставил его работать, а не только потому, что все его части оказались там. Если вы запрашиваете все записи и ваш код работает, то у вас будет тенденция двигаться дальше. Позже, если что-то изменится, и теперь ваш код не работает, это большая проблема для отладки множества запросов и функций, ищущих значение, которое должно быть там, и единственная ссылка на значения - *

Также в N-уровневом подходе все еще лучше изолировать сбои схемы базы данных на уровне данных. Если ваш уровень данных переходит * в бизнес-логику и, скорее всего, в уровень представления, вы расширяете область отладки в геометрической прогрессии.


3
Это, вероятно, одна из самых важных причин здесь, и она получает лишь крошечную долю голосов. Сопровождаемость кодовой базы изобилует select *гораздо хуже!
Имон Нербонн

6

потому что если таблица получает новые столбцы, вы получаете все эти столбцы, даже если они вам не нужны. с varcharsэтим может стать много лишних данных, которые нужно перемещать из БД

некоторые оптимизации БД могут также извлекать записи не фиксированной длины в отдельный файл, чтобы ускорить доступ к частям фиксированной длины, используя select *, что побеждает цель этого


1

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


3
Согласитесь, хотя я бы также рекомендовал в любом случае извлекать значения из результирующего набора по имени столбца.
Рори Хантер

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

1

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

  1. Столбец добавляется в базу данных, и вы хотите его в коде также. a) С * произойдет сбой с правильным сообщением. б) Без * будет работать, но не будет делать то, что вы ожидаете, что довольно плохо.

  2. Столбец добавляется в базу данных, и вы не хотите его в коде. а) С * не удастся; это означает, что * больше не применяется, так как его семантика означает «получить все». б) без * будет работать.

  3. Столбец удален. Код не работает в любом случае.

Теперь наиболее распространенным случаем является случай 1 (поскольку вы использовали *, что означает все, что вы, скорее всего, хотите всего); без * вы можете иметь код, который работает нормально, но не делает то, что ожидалось, что намного, гораздо хуже, чем код, который завершается ошибкой с правильным сообщением об ошибке .

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


Ваша предпосылка неверна. Select *было задумано скорее как удобство для специальных запросов, а не для целей разработки приложений. Или для использования в статистических конструкциях, подобных select count(*)которым позволяет обработчику запросов решать, использовать ли индекс, какой индекс использовать и т. Д., И вы не возвращаете никаких фактических данных столбца. Или для использования в таких предложениях, как where exists( select * from other_table where ... ), что опять-таки является приглашением в механизм запросов самостоятельно выбирать наиболее эффективный путь, а подзапрос используется только для ограничения результатов основного запроса. И т.д.
Крэйг

@Craig Я считаю, что каждая книга / учебник по SQL говорит, что select *имеет семантику извлечения всех столбцов; если ваше приложение действительно нуждается в этом, я не вижу причин, почему бы не использовать его. Можете ли вы указать на какую-то ссылку (Oracle, IBM, Microsoft и т. Д.), В которой упоминается, что целью select *сборки была не получение всех столбцов?
m3th0dman

Ну, конечно, select *существует для извлечения всех столбцов ... в качестве удобной функции, для специальных запросов, а не потому, что это отличная идея в производственном программном обеспечении. Причины уже достаточно хорошо освещены в ответах на этой странице, поэтому я не создал свой собственный подробный ответ: •) Проблемы с производительностью, многократное распределение данных по сети, которые вы никогда не используете, •) проблемы с алиасами столбцов, •) ошибки оптимизации плана запросов (в некоторых случаях отказ от использования индексов), •) неэффективный серверный ввод-вывод в случаях, когда ограниченный выбор мог использовать только индексы и т. Д.
Крейг,

Может быть , здесь или там есть крайний случай, который оправдывает использование select *в реальном производственном приложении, но природа крайнего случая состоит в том, что это не общий случай. :-)
Крейг,

@Craig Причины против извлечения всех столбцов из базы данных, а не против использования select *; то, что я говорил, если вам действительно нужны все столбцы, я не вижу причин, почему вы не должны их использовать select *; хотя немногие должны быть сценарии, где нужны все столбцы.
m3th0dman

1

Подумайте об этом так ... если вы запрашиваете все столбцы из таблицы, в которой есть всего несколько небольших строковых или числовых полей, то это 100 000 данных. Плохая практика, но она сработает. Теперь добавьте одно поле, которое содержит, скажем, изображение или документ размером 10 МБ. теперь ваш быстродействующий запрос немедленно и загадочно начинает работать плохо, просто потому, что в таблицу было добавлено поле ... вам может не понадобиться этот огромный элемент данных, но, поскольку вы это сделали, Select * from Tableвы все равно получите его.


6
это, кажется, просто повторяет точку зрения, уже высказанную несколько часов назад в первом ответе и в нескольких других ответах
комнат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.