The primary defense is parameterized queries everywhere user input touches SQL. Most languages and ORMs do this by default: in Python use psycopg with cursor.execute(query, params), in Node use the pg library with $1 placeholders, in Java use PreparedStatement. An ORM like Drizzle, Prisma, or Hibernate generates parameterized SQL for you, which is why ORMs cut down injection bugs, but the protection breaks the moment you drop to raw string SQL or build a query with string concatenation.
Input validation and allowlisting help as a second layer, especially for things that cannot be parameterized such as table names, column names, or ORDER BY directions. For those, check the value against a fixed list of permitted names rather than escaping it. Escaping input by hand is fragile and easy to get wrong across different databases, so it should be a last resort, not the main strategy.
Defense in depth limits the damage if injection still happens. Give the application database account only the privileges it needs (a read API user should not be able to DROP tables), put a web application firewall in front to catch obvious payloads, and turn off detailed SQL error messages in production so attackers cannot use them to map your schema. The cost of all this is mostly discipline, not performance, since prepared statements are usually as fast or faster because the database caches the compiled plan.